using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Web;
using System.Web.Caching;
using Microsoft.Practices.EnterpriseLibrary.Data;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;


namespace Microsoft.Practices.Repository
{
    /// <summary>
    /// A generic repository base class that uses various
    /// factory classes to get and retrieve specific domain
    /// objects.
    /// </summary>
    public abstract class Repository<TDomainObject>
    {
        private readonly string databaseName;
        private Database db;
        private string conString;

        /// <summary>
        /// Creates an instance of Repository class.
        /// </summary>
        /// <param name="databaseName">The database name.</param>
        protected Repository(string databaseName)
        {
            this.databaseName = databaseName;
            this.Database = DatabaseFactory.CreateDatabase(databaseName);
        }


        /// <summary>
        /// Used by the Sql Caching functions.
        /// </summary>
        private string ConnectionString
        {
            get
            {
                if (string.IsNullOrEmpty(conString))
                {
                    conString = ConfigurationManager.ConnectionStrings[databaseName].ConnectionString;

                }
                return conString;
            }
        }


        /// <summary>
        /// Allows the database property to be overridden.
        /// </summary>
        protected Database Database
        {
            get
            {
                return db;
            }
            set
            {
                db = value;
            }
        }


        /// <summary>
        /// Creates an instance of Repository class using the default database name
        /// specified on the configuration file.
        /// </summary>
        protected Repository()
        {
            this.Database = DatabaseFactory.CreateDatabase();
        }

        /// <summary>
        /// Find all objects that match the given criteria.
        /// </summary>
        /// <typeparam name="TIdentity">Type of object used to identify
        /// the objects to find.</typeparam>
        /// <param name="selectionFactory">Factory object that can turn the
        /// identity object into the appropriate DbCommand.</param>
        /// <param name="domainObjectFactory">Factory object that can turn the
        /// returned result set into a domain object.</param>
        /// <param name="identity">Object that identifies which items to get.</param>
        /// <returns></returns>
        protected List<TDomainObject> Find<TIdentity>(
            ISelectionFactory<TIdentity> selectionFactory,
            IDomainObjectsFactory<TDomainObject> domainObjectFactory,
            TIdentity identity)
        {
            List<TDomainObject> results = new List<TDomainObject>();

            using (DbCommand command = selectionFactory.ConstructSelectCommand(this.Database, identity))
            {
                IDataReader rdr = this.Database.ExecuteReader(command);
                {
                    try
                    {
                        this.ConfigureDataReader(rdr);
                        // The domainObjectFactory is responsible for iterating through the 
                        // ref cursor(s) and constructing a List<TDomainObject>.
                        results.AddRange(domainObjectFactory.Construct(rdr));
                    }
                    finally
                    {
                        ConsumeUnretrievedCursors(rdr);
                        rdr.Dispose();
                    }
                }
            }
            return results;
        }

        /// <summary>
        /// Find all objects that match the given criteria using Sql cache.
        /// </summary>
        /// <typeparam name="TIdentity">Type of object used to identify the objects to find.</typeparam>
        /// <param name="selectionFactory">Factory object that can turn the identity object into the appropriate DbCommand.</param>
        /// <param name="domainObjectFactory">Factory object that can turn the returned result set into a domain object.</param>
        /// <param name="identity">Object that identifies which items to get.</param>
        /// <param name="cacheKey"></param>
        /// <param name="expiry"></param>
        /// <returns></returns>
        protected List<TDomainObject> FindWithSqlCache<TIdentity>(
            ISelectionFactory<TIdentity> selectionFactory,
            IDomainObjectFactory<TDomainObject> domainObjectFactory,
            TIdentity identity,
            string cacheKey,
            TimeSpan expiry)
        {
            List<TDomainObject> results = new List<TDomainObject>();

            using (SqlCommand command = (SqlCommand)selectionFactory.ConstructSelectCommand(this.Database, identity))
            {
                SqlDependency.Start(this.ConnectionString);
                SqlCacheDependency dependency = new SqlCacheDependency(command);

                using (IDataReader rdr = this.Database.ExecuteReader(command))
                {
                    this.ConfigureDataReader(rdr);
                    while (rdr.Read())
                    {
                        results.Add(domainObjectFactory.Construct(rdr));
                    }
                }
                HttpContext.Current.Cache.Add(cacheKey, results, dependency, Cache.NoAbsoluteExpiration, expiry, CacheItemPriority.Normal, null);
            }
            return results;
        }


        /// <summary>
        /// Find the first / only object that matches the given criteria.
        /// </summary>
        /// <typeparam name="TIdentity">Type of object used to identity the domain object desired.</typeparam>
        /// <param name="selectionFactory">Factory object that can turn
        /// the identity object into the appropriate DbCommand.</param>
        /// <param name="domainObjectFactory">Factory object that can turn the
        /// returned result set into a domain object.</param>
        /// <param name="identity">Object that identifies which item to get.</param>
        /// <returns>The domain object requested, or null if not found.</returns>
        protected TDomainObject FindOne<TIdentity>(
            ISelectionFactory<TIdentity> selectionFactory,
            ISimpleDomainObjectFactory<TDomainObject> domainObjectFactory,
            TIdentity identity)
        {
            TDomainObject result = default(TDomainObject);
            using (DbCommand command = selectionFactory.ConstructSelectCommand(this.Database, identity))
            {
                db.ExecuteNonQuery(command);
                result = domainObjectFactory.Construct(this.Database, command);
            }
            return result;
        }

        /// <summary>
        /// Find the first / only object that matches the given criteria.
        /// </summary>
        /// <typeparam name="TIdentity">Type of object used to identity the domain object desired.</typeparam>
        /// <param name="selectionFactory">Factory object that can turn the identity object into the appropriate DbCommand.</param>
        /// <param name="domainObjectFactory">Factory object that can turn the returned result set into a domain object.</param>
        /// <param name="identity">Object that identifies which item to get.</param>
        /// <param name="cacheKey"></param>
        /// <param name="expiry"></param>
        /// <returns></returns>
        protected TDomainObject FindOneWithSqlCache<TIdentity>(
            ISelectionFactory<TIdentity> selectionFactory,
            IDomainObjectFactory<TDomainObject> domainObjectFactory,
            TIdentity identity,
            string cacheKey,
            TimeSpan expiry)
        {
            TDomainObject result = default(TDomainObject);
            using (SqlCommand command = (SqlCommand)selectionFactory.ConstructSelectCommand(this.Database, identity))
            {
                SqlDependency.Start(this.ConnectionString);
                SqlCacheDependency dependency = new SqlCacheDependency(command);

                using (IDataReader rdr = this.Database.ExecuteReader(command))
                {
                    this.ConfigureDataReader(rdr);
                    if (rdr.Read())
                    {
                        result = domainObjectFactory.Construct(rdr);
                    }
                }

                HttpContext.Current.Cache.Add(cacheKey, result, dependency, Cache.NoAbsoluteExpiration, expiry, CacheItemPriority.Normal, null);
            }
            return result;
        }

        /// <summary>
        /// Find an object that contains a collection of objects.  For example, you could retrieve an order
        /// and related order details.
        /// </summary>
        /// <typeparam name="TIdentity">Type of object used to identity the domain object desired.</typeparam>
        /// <param name="selectionFactory">Factory object that can turn
        /// the identity object into the appropriate DbCommand.</param>
        /// <param name="domainObjectFactory">Factory object that can turn the
        /// returned result set into a domain object.</param>
        /// <param name="identity">Object that identifies which item to get.</param>
        /// <returns>The domain object requested, or null if not found.</returns>
        protected TDomainObject FindOneComplex<TIdentity>(
            ISelectionFactory<TIdentity> selectionFactory,
            IComplexDomainObjectFactory<TDomainObject> domainObjectFactory,
            TIdentity identity)
        {
            TDomainObject result = default(TDomainObject);
            using (DbCommand command = selectionFactory.ConstructSelectCommand(this.Database, identity))
            {
                IDataReader rdr = this.Database.ExecuteReader(command);
                {
                    try
                    {
                        this.ConfigureDataReader(rdr);
                        result = domainObjectFactory.Construct(this.Database, command, rdr);
                    }
                    finally
                    {
                        ConsumeUnretrievedCursors(rdr);
                        rdr.Dispose();
                    }
                }
            }
            return result;
        }

        /// <summary>
        /// Insert the given object into the database.
        /// </summary>
        /// <param name="insertFactory">Factory used to create the command.</param>
        /// <param name="domainObject">Domain object to insert</param>
        protected void Add(IInsertFactory<TDomainObject> insertFactory,
            TDomainObject domainObject)
        {
            using (DbCommand command = insertFactory.ConstructInsertCommand(this.Database, domainObject))
            {
                this.Database.ExecuteNonQuery(command);
                insertFactory.SetNewId(this.Database, command, domainObject);
            }
        }

        /// <summary>
        /// Inserts the given set of objects into the database.
        /// </summary>
        /// <param name="bulkInsertFactory">Factory used to create the command.</param>
        /// <param name="domainObjects">The set of domain objects to insert.</param>
        protected void Add(IBulkInsertFactory<TDomainObject> bulkInsertFactory, IEnumerable<TDomainObject> domainObjects)
        {
            using (DbCommand command = bulkInsertFactory.ConstructInsertCommand(this.Database, domainObjects))
            {
                this.Database.ExecuteNonQuery(command);
            }
        }

        /// <summary>
        /// Removes the given object from the database.
        /// </summary>
        /// <typeparam name="TIdentityObject">Identity object class type.</typeparam>
        /// <param name="deleteFactory">Factory used to create the command.</param>
        /// <param name="identityObj">The identity object.</param>
        protected void Remove<TIdentityObject>(IDeleteFactory<TIdentityObject> deleteFactory,
            TIdentityObject identityObj)
        {
            using (DbCommand command = deleteFactory.ConstructDeleteCommand(this.Database, identityObj))
            {
                this.Database.ExecuteNonQuery(command);
            }
        }

        /// <summary>
        /// Removes a set of domain objects from the database based on the identities.
        /// </summary>
        /// <typeparam name="TIdentityObject">Identity object class type.</typeparam>
        /// <param name="bulkDeleteFactory">The factory used to create the command.</param>
        /// <param name="identityObjects">The identity objects.</param>
        protected void Remove<TIdentityObject>(IBulkDeleteFactory<TIdentityObject> bulkDeleteFactory,
            IEnumerable<TIdentityObject> identityObjects)
        {
            using (DbCommand command = bulkDeleteFactory.ConstructDeleteCommand(this.Database, identityObjects))
            {
                this.Database.ExecuteNonQuery(command);
            }
        }

        /// <summary>
        /// Updates a specific domain object instance on the database.
        /// </summary>
        /// <param name="updateFactory">Factory used to create the command.</param>
        /// <param name="domainObj">The domain object to update.</param>
        protected void Save(IUpdateFactory<TDomainObject> updateFactory, TDomainObject domainObj)
        {
            using (DbCommand command = updateFactory.ConstructUpdateCommand(this.Database, domainObj))
            {
                this.Database.ExecuteNonQuery(command);
            }
        }

        /// <summary>
        /// Updates a set of domain objects to the database.
        /// </summary>
        /// <param name="bulkUpdateFactory">Factory used to create the command.</param>
        /// <param name="domainObjects">The domain objects to update.</param>
        protected void Save(IBulkUpdateFactory<TDomainObject> bulkUpdateFactory, IEnumerable<TDomainObject> domainObjects)
        {
            using (DbCommand command = bulkUpdateFactory.ConstructUpdateCommand(this.Database, domainObjects))
            {
                this.Database.ExecuteNonQuery(command);
            }
        }

        /// <summary>
        /// Allows sub-classes to configure the reader with specifics 
        /// related to the provider.  For example, you could set the FetchSize 
        /// property of an OracleDataReader.
        /// </summary>
        /// <param name="reader">The IDataReader returned by the command.</param>
        protected virtual void ConfigureDataReader(IDataReader reader)
        {
            return;
        }

        /// <summary>
        /// Accesses any remaining ref cursors accessible to the reader.
        /// This allows ODP to properly close them on the server.
        /// </summary>
        /// <remarks>
        /// This was to work around an issue identified in ODP 2.111.6.20:
        /// When calling a command that returns multiple ref cursors,
        /// if the consumer of the cursors fails and exits before accessing all cursors,
        /// remaining cursors get left open on the database.  
        /// This is despite calling dispose on the reader and command.
        /// </remarks>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private static void ConsumeUnretrievedCursors(IDataReader rdr)
        {
            try
            {
                if (rdr != null && !rdr.IsClosed)
                    while (rdr.NextResult()) ;
            }
            catch
            {
                //no reason to fail while consuming unused cursors 
            }
        }
    }
}
